# -*- coding: utf-8 -*-
"""
    adrianremembersmeapp.adrianremembersme
    =======================================

    Database interface to the GAE Models
"""
import random
import string
import logging
import urllib
import hashlib
import datetime
import os, re

from google.appengine.ext import db
from google.appengine.api import images, mail

# Django 1.2 changed validators from forms.fields to core.validators
from django.conf import settings
from django.http import HttpResponseRedirect

from django.core.exceptions import ValidationError

import armsettings , ratelimiter
from models import Member, LoginRecord, Photo, PasswordResetLink

# TODO: Add RateLimiter to certain methods.

class AdrianRemembersMe():
    """
    Holds the general methods of login,logout,member retrieval and other
    common operations. Typical usage::

        loggedmember = AdrianRemembersMe().is_logged( request.COOKIES.get('user',''))
        if loggedmember:
            return HttpResponse("Welcome %s" % loggedmember.name)
        else:
            return HttpResponseRedirect("/")
    """

    def get(self, username):
        """
        Returns the given member by his username.
        """
        username = username and username.lower()
        return db.GqlQuery("SELECT * FROM Member WHERE name = :1", username).get()

    def username_available(self, username):
        """
        Username is available?
        """
        username = username and username.lower()
        available = not bool(db.GqlQuery("SELECT * FROM Member WHERE name = :1", username ).get())
        return (not armsettings.USERNAME_BOOKING and available or
               (username not in armsettings.USERNAME_BOOKING_LIST and available) )        

    def email_available(self, email):
        email = email.lower()
        return not bool(db.GqlQuery("SELECT * FROM Member WHERE email = :1", email ).get())



    def logout( self, username=None, cookie=None ):
        """
        Logout a user by his username or cookie
        """
        username = username and username.lower()
        query = (username and "SELECT * FROM LoginRecord WHERE member = :1" or
                            "SELECT * FROM LoginRecord WHERE code = :1" )
        param = (username or cookie )

        q = db.GqlQuery(query, param)
        [login.delete() for login in q.fetch(10)]

    def logout_all_sessions( self, username, cookie_val):
        """
        Force user X to logout from all machines, except the current logged cookie_val
        """
        username = username and username.lower()
        q = db.GqlQuery("SELECT * FROM LoginRecord WHERE member = :1" , username )
        [login.delete() for login in q.fetch(10) if login.code != cookie_val]

    def logged_users( self, amount=50 ):
        """ Get everyone who is logged in """
        return db.GqlQuery("SELECT * FROM LoginRecord").fetch(amount)

    def authenticate(self, username, password, ip_address ):
        """
        If the combination of username/password is correct, returns a member. False, otherwise.

       .. note:: You have to take care of users behind proxies (X_FORWARDED_FOR), since IDS may
                 give false positives in this context
        """
        username = username and username.lower()
        member = db.GqlQuery("SELECT * FROM Member WHERE name = :1 LIMIT 1", username).get()
        if member:
            intrusion_detector = ratelimiter.LoginIntrusionDetector( ip_address, username, password )
            try:
                intrusion_detector.caution_check()
            except ratelimiter.Intrusion, e:
                return False

            if self.check_password( member, password ):
                return member
            else:
                intrusion_detector.caution_on_retry()

        return False


    def login(self, member, rememberme=False, response=HttpResponseRedirect("/") ):
        """ Persist effectively a member. This way a member doesn't have to reauthenticate
            on every request.

            By default, returns a HTTP Redirect to the root of the site with a special
            cookie appended.

            .. note:: Don't forget to send this response to the user!
        """
        cookie_val = self.generate_random(128)
        login_rec        = LoginRecord()
        login_rec.code   = cookie_val
        login_rec.member = member
        login_rec.put()

        response.set_cookie('user', urllib.quote(cookie_val),
                            expires=(rememberme and 'Fri, 31-Dec-2030 23:59:59 GMT' or None))
	
        return response

    def is_logged(self, username=None, cookie_val=None):
        """
        Check if given cookie or user is currently logged.

        Returns the user if logged, None otherwise
        """
        username = username and username.lower()
        query = (username and "SELECT * FROM LoginRecord WHERE member = :1" or
                            "SELECT * FROM LoginRecord WHERE code = :1" )
        param = (username or cookie_val )

        login_record = db.GqlQuery(query,param).get()
        return (login_record and login_record.member or None)


    def signup( self, username, password, fullname, email, photo = None ):
        """
        Signup a new member with an optional photo. User should confirm his email account
        with the returned validation code.

        """
        salt = self.generate_random()
        if armsettings.ENCRYPTED_PASSWORDS:
            password = self.generate_hash( password, salt )

        # Save Member
        username = username.lower()
        email = email.lower()
        member                        = Member()
        member.salt , member.password = salt       , password
        member.name , member.fullname = username   , fullname
        emailcode                     = self.save_email(member, email)
        member.put()

        # Save profile picture
        if photo:
            self.save_photo( member, photo.read(), photo.name )
	
        return (member, emailcode)


    def save_photo(self, member, photo_bytestring, photo_name ):
        """
        Save the member profile photo.

        :warning: The old photo is lost but not deleted. This is the correct behaviour?
        """
        photo = db.GqlQuery("SELECT * FROM Photo WHERE member = :1", member.name).get()

        # Create photo, if necessary
        if not photo:
            photo = Photo()
            photo.member = member

        # Pil may have refuse if file is not valid. In that case, just
        # proceeds since no profile picture will be saved. Secure, but not
        # much user-friendly.
        try:
            photo.filename = photo_name
            photo.content  = images.resize( photo_bytestring, output_encoding=images.JPEG, **armsettings.PROFILE_IMAGE_SIZE )
            photo.thumb    = images.resize( photo_bytestring, output_encoding=images.PNG, **armsettings.THUMBNAIL_SIZE)
            logging.info("Photo filename: %s", photo.filename)
            photo.put()
            return True
        except:
            return False


    def save_email(self, member, email ):
        """
        Set the email account, returns a validation code.

        .. note:: Email should be verified prior to saving.
        """
        email = email.lower()
        member.email               = email
        member.emailisvalid        = False
        member.emailvalidationcode = self.generate_random()
        return member.emailvalidationcode



    def validate_emailcode( self, code ):
        """
        The given validation email code is correct?
        """
        member = db.GqlQuery("SELECT * FROM Member WHERE emailvalidationcode = :1 AND \
                              emailisvalid = :2" , code ,False).get()
        if member:
            member.emailisvalid = True
            member.put()

        return (member or False)


    def check_password(self,member, password):
        """
        Returns a boolean of whether the given password was correct. Handles
        encryption behind the scenes.
        """
        password = (armsettings.ENCRYPTED_PASSWORDS and self.generate_hash(password, member.salt) or password)
        return password == member.password

    def change_password(self, member, newpass ):
        """
        Changes the current password. Please check strength before
        saving.
        """
        if armsettings.ENCRYPTED_PASSWORDS:
            if not member.salt:
                member.salt = self.generate_random()

            newpass = self.generate_hash(newpass, member.salt)

        member.password = newpass
        member.put()

    def forgot_password(self, email):
        """
        Returns a resetpass token for people who needs to reset the password. Don't forget
        to send this code to `reset_password` and ``
        """
        member = db.GqlQuery("SELECT * FROM Member WHERE email = :1 LIMIT 1", email).get()

        # TODO: Add RateLimiter
        if member:
            token = self.generate_reset_token(member)
            return {'member': member, 'token': token.code }

        return False

    def generate_reset_token( self, member ):
        """
        Returns a reset token for given member (valid for 24 hours).
        """
        token                  = self.generate_random(128)
        reset_link             = PasswordResetLink()
        reset_link.member      = member
        reset_link.code        = token
        reset_link.alreadyused = False
        reset_link.put()

        return reset_link

    def invalidate_reset_token(self,token):
        """ Invalidates the given token forever. """
        reset_link = db.GqlQuery("SELECT * FROM PasswordResetLink WHERE code = :1 AND alreadyused = :2 LIMIT 1", token, False).get()
        reset_link.alreadyused = True
        reset_link.put()


    def retrieve_reset_token(self,token):
        """ Validates the given token. Returns a member if valid , None otherwise. """
        reset_link = db.GqlQuery("SELECT * FROM PasswordResetLink WHERE code = :1 AND alreadyused = :2 LIMIT 1", token, False).get()

        if reset_link:
            earliestvaliddate = datetime.datetime.now() - datetime.timedelta(hours=24)
            if reset_link.timeadded > earliestvaliddate:
                return reset_link.member
            else:
                self.invalidate_reset_token( token )

        return None

###########
# hashing #
###########

    def generate_random(self,saltlength=25):
        """ Generate a random string (chars+punctuation+numbers) with the given length

            .. note:: While using this strategy on regular python website is not secure(
                    due to the MersenneTwister PRNG - random.choice), GAE takes care of entropy
                    sourcing for us (and since each process is short-lived, we are safe of sampling attacks).
                    `GAE Random considerations<http://code.google.com/p/googleappengine/issues/detail?id=1055>`.
        """
        return ''.join([random.choice(string.ascii_letters + string.digits)
                        for x in xrange(saltlength)])

    def generate_hash(self,value, prefixsalt='', postfixsalt=''):
        """ Returns an hexdigest of sha512(value) (with pre+post salt) """
        if isinstance(value, unicode):
            value = value.encode('utf-8')
        pre , post = str(prefixsalt) , str(postfixsalt)
        value      = pre + value + post
        h          = hashlib.sha512()
        h.update(value)
        return h.hexdigest()

    #def encryptpasswords(self):
        #"""
            #"Encrypt" password. Actually, this name is wrong, we just
            #generate a sha512 hash of the password
        #"""
        #if not armsettings.ENCRYPTED_PASSWORDS:
            #return False
        #query = Member.all()
        #for member in query:
            #if not member.salt or len(member.password) < 128:
                #salt            = self.generate_random()
                #password        = self.generate_hash(member.password,salt)
                #member.salt     = salt
                #member.password = password
                #member.put()
        #return True
